package com.googlecode.stateless4j;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.ListUtils;

import com.googlecode.stateless4j.delegates.TransitionedRunnable;
import com.googlecode.stateless4j.transitions.Transition;
import com.googlecode.stateless4j.triggers.TriggerBehaviour;
import com.googlecode.stateless4j.validation.ArgumentValidator;
import com.googlecode.stateless4j.validation.MultipleTransitionsPermittedException;
import com.googlecode.stateless4j.validation.NoHandlerForBehaviourException;

/**
 * State Representation
 *
 * @param <TState>
 * @param <TTrigger>
 */
public class StateRepresentation<TState, TTrigger>
{
	/**
     * Internal class to keep track of runnables and associated triggers.
     *
     */
    private class TriggeredRunnable
    {
        private Runnable runnable;
        private TTrigger trigger;

        /**
         * @param runnable
         * @param trigger
         */
        public TriggeredRunnable(Runnable runnable, TTrigger trigger)
        {
            super();
            this.runnable = runnable;
            this.trigger = trigger;
        }

        /**
         * @return the runnable
         */
        public Runnable getRunnable()
        {
            return runnable;
        }

        /**
         * @return the trigger
         */
        public TTrigger getTrigger()
        {
            return trigger;
        }

        public boolean hasTrigger()
        {
            return trigger != null;
        }
    }
    
    private final TState currentState;
    private static final String MultipleTransitionsPermitted = "MultipleTransitionsPermitted";

    private final Map<TTrigger, List<TriggerBehaviour<TState, TTrigger>>> triggerBehaviours = new HashMap<TTrigger, List<TriggerBehaviour<TState, TTrigger>>>();

    private final List<TriggeredRunnable> entryRunnables = new ArrayList<TriggeredRunnable>();
    private final List<TriggeredRunnable> exitRunnables = new ArrayList<TriggeredRunnable>();

    private StateRepresentation<TState, TTrigger> superstate; // null

    private final List<StateRepresentation<TState, TTrigger>> substates = new ArrayList<StateRepresentation<TState, TTrigger>>();

	/**
     * @param state
     */
    public StateRepresentation(TState state)
    {
        currentState = state;
    }
    
    /**
     * @return unmodifiable map of trigger behaviors.
     */
    public Map<TTrigger, List<TriggerBehaviour<TState, TTrigger>>> getTriggerBehaviours()
    {
        return Collections.unmodifiableMap(triggerBehaviours);
    }

	/**
     * @param trigger
     * @return true if handles trigger, false otherwise.
     */
    public Boolean canHandle(TTrigger trigger)
    {
        try
        {
            tryFindHandler(trigger);
            return true;
        }
        catch (Exception e)
        {
        }
        return false;
    }

	/**
     * TODO: determine best approach for not finding TTrigger condition.
     * 
     * @param trigger
     * @return
     */
    public TriggerBehaviour<TState, TTrigger> tryFindHandler(TTrigger trigger)
    {
        try
        {
            return tryFindLocalHandler(trigger);
        }
        catch (Exception e)
        {
            return getSuperstate().tryFindHandler(trigger);
        }
    }

	/**
     * @param trigger
     * @return
     * @throws MultipleTransitionsPermittedException
     * @throws NoHandlerForBehaviourException
     */
    TriggerBehaviour<TState, TTrigger> tryFindLocalHandler(TTrigger trigger/*, out TriggerBehaviour handler*/)
        throws MultipleTransitionsPermittedException, NoHandlerForBehaviourException
    {
        List<TriggerBehaviour<TState, TTrigger>> possible;
        if (!triggerBehaviours.containsKey(trigger))
        {
            throw new IllegalArgumentException("Trigger does not exist: " + trigger);
        }
        possible = triggerBehaviours.get(trigger);

        List<TriggerBehaviour<TState, TTrigger>> actual = new ArrayList<TriggerBehaviour<TState, TTrigger>>();
        for (TriggerBehaviour<TState, TTrigger> triggerBehaviour : possible)
        {
            if (triggerBehaviour.isGuardConditionMet())
            {
                actual.add(triggerBehaviour);
            }
        }

        if (actual.size() > 1)
            throw new MultipleTransitionsPermittedException(String.format(MultipleTransitionsPermitted,
                trigger, currentState));

        TriggerBehaviour<TState, TTrigger> handler = actual.get(0);
        if (handler == null)
        {
            throw new NoHandlerForBehaviourException();
        }
        return handler;
    }

    public void addEntryAction(final TTrigger trigger, final Runnable action)
    {
        ArgumentValidator.argumentNotNull(action, "action");
        entryRunnables.add(new TriggeredRunnable(action, trigger));

    }

    public void addEntryAction(Runnable action)
    {
        entryRunnables.add(new TriggeredRunnable(action, null));
    }

    public void addExitAction(Runnable action)
    {
        ArgumentValidator.argumentNotNull(action, "action");

        exitRunnables.add(new TriggeredRunnable(action, null));
    }

    public void enter(Transition<TState, TTrigger> transition, Object... entryArgs)
    {
        ArgumentValidator.argumentNotNull(transition, "transtion");

        if (transition.isReentry())
        {
            executeEntryActions(transition, entryArgs);
        }
        else if (!includes(transition.getSource()))
        {
            if (superstate != null)
                superstate.enter(transition, entryArgs);

            executeEntryActions(transition, entryArgs);
        }
    }

    public void exit(Transition<TState, TTrigger> transition)
    {
        ArgumentValidator.argumentNotNull(transition, "transtion");

        if (transition.isReentry())
        {
            executeExitActions(transition);
        }
        else if (!includes(transition.getDestination()))
        {
            executeExitActions(transition);
            if (superstate != null)
                superstate.exit(transition);
        }
    }

    @SuppressWarnings("unchecked")
    void executeEntryActions(Transition<TState, TTrigger> transition, Object[] entryArgs)
    {
        ArgumentValidator.argumentNotNull(transition, "transtion");
        ArgumentValidator.argumentNotNull(entryArgs, "entryArgs");
        for (TriggeredRunnable action : entryRunnables)
        {
            if (!action.hasTrigger() || action.getTrigger().equals(transition.getTrigger()))
            {
                Runnable runnable = action.getRunnable();
                if (runnable instanceof TransitionedRunnable)
                {
                    ((TransitionedRunnable<TState, TTrigger>) runnable).setTransition(transition);
                }

                runnable.run();
            }
        }
    }

    @SuppressWarnings("unchecked")
    void executeExitActions(Transition<TState, TTrigger> transition)
    {
        ArgumentValidator.argumentNotNull(transition, "transtion");
        for (TriggeredRunnable triggeredRunnable : exitRunnables)
        {
            if (!triggeredRunnable.hasTrigger() || triggeredRunnable.getTrigger().equals(transition.getTrigger()))
            {
                Runnable runnable = triggeredRunnable.getRunnable();
                if (runnable instanceof TransitionedRunnable)
                {
                    ((TransitionedRunnable<TState, TTrigger>) runnable).setTransition(transition);
                }

                runnable.run();
            }
        }
    }

    public void addTriggerBehaviour(TriggerBehaviour<TState, TTrigger> triggerBehaviour)
    {
        List<TriggerBehaviour<TState, TTrigger>> allowed;
        if (!triggerBehaviours.containsKey(triggerBehaviour.getTrigger()))
        {
            allowed = new ArrayList<TriggerBehaviour<TState, TTrigger>>();
            triggerBehaviours.put(triggerBehaviour.getTrigger(), allowed);
        }
        allowed = triggerBehaviours.get(triggerBehaviour.getTrigger());
        allowed.add(triggerBehaviour);
    }

    public StateRepresentation<TState, TTrigger> getSuperstate()
    {
        return superstate;
    }

    public void setSuperstate(StateRepresentation<TState, TTrigger> value)
    {
        superstate = value;
    }

    public TState getUnderlyingState()
    {
        return currentState;
    }

    public void addSubstate(StateRepresentation<TState, TTrigger> substate)
    {
        ArgumentValidator.argumentNotNull(substate, "substate");
        substates.add(substate);
    }

    public Boolean includes(TState state)
    {
        Boolean isIncluded = false;
        for (StateRepresentation<TState, TTrigger> s : substates)
        {
            if (s.includes(state))
            {
                isIncluded = true;
            }
        }
        return currentState.equals(state) || isIncluded;
    }

    public Boolean includedIn(TState state)
    {
        return currentState.equals(state) || (superstate != null && superstate.includedIn(state));
    }

    @SuppressWarnings("unchecked")
    public List<TTrigger> getPermittedTriggers()
    {
        List<TTrigger> result = new ArrayList<TTrigger>();

        for (TTrigger t : triggerBehaviours.keySet())
        {
            Boolean isOk = false;
            for (TriggerBehaviour<TState, TTrigger> v : triggerBehaviours.get(t))
            {
                if (v.isGuardConditionMet())
                {
                    isOk = true;
                }
            }
            if (isOk)
            {
                result.add(t);
            }
        }

        if (getSuperstate() != null)
            result = ListUtils.sum(result, getSuperstate().getPermittedTriggers());

        return result;
    }
}